package com.lkf.simplewebcontainer.container;

import com.lkf.simplewebcontainer.servlet.Servlet;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.util.internal.StringUtil;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 网络容器服务器的简单实现
 * <p>
 * 容器包含：
 * 1.servletNameMap
 * Servlet简单类名与Sevlet全限定类名对应的集合
 * <p>
 * 2.servletMap
 * Servlet简单类名与Servlet实例对应的集合
 * 当客户端request，根据request当中请求的Servlet名称，查找实例集合中有无该Servlet的实例
 * 若存在，直接拿来使用执行业务，避免重新创建Servlet实例，减少资源消耗
 * 若不存在，根据servletNameMap集合找到Servlet的全限定类名，使用反射创建Servlet实例装入集合，并使用该Servlet执行业务
 * <p>
 * 3.packagePath
 * Servlet所在包名，从静态资源获取，可自由配置包名
 * <p>
 * 4.cacheClassName
 * 得到包名之下所有Servlet实现类得类名和全限定类名并装入servletNameMap中
 * 程序编译后会生成所有Sevlet对应的class文件，利用该特点，容器初始化时，从Servlet包路径下的class文件中找到所有Servlet类的简单类名和全限定类名装入集合
 * <p>
 * 5.initContent
 * 从配置文件中读取端口号和Servlet所在包名
 * <p>
 * 6.start
 * 启动网络容器服务
 */
public class WebServer {

    // Servlet包地址
    private static String packagePath;
    // Server占用端口号
    private static int port;
    // 静态资源地址
    private static String staticPath;
    // Servlet实例集合
    private Map<String, Servlet> servletMap = new ConcurrentHashMap<>();
    // Servlet类名集合
    private Map<String, String> servletNameMap = new HashMap<>();

    public WebServer() {
        initContent();
    }

    /**
     * 启动容器服务
     *
     * @throws Exception
     */
    public void start() throws Exception {
        cacheClassName(packagePath);
        runServer();
    }

    /**
     * 得到包名之下所有Servlet实现类得类名和全限定类名并装入servletNameMap中
     * 程序编译后会生成所有Sevlet对应的class文件，利用该特点，从Servlet包路径下的class文件中找到所有Servlet类的简单类名和全限定类名装入集合
     *
     * @param packagePath 包路径
     */
    private void cacheClassName(String packagePath) {

        if (!StringUtil.isNullOrEmpty(packagePath)) {
            // 获取指定包中的资源
            URL resource = this.getClass().getClassLoader()
                    // 将包名中的.替换为/，便于找到对应文件
                    .getResource(packagePath.replaceAll("\\.", "/"));
            // 若目录中没有任何资源，则直接结束
            if (resource == null) {
                System.out.println(packagePath + "中没有任何资源！");
                return;
            }
            // 将URL资源转换为File资源
            File dir = new File(resource.getFile());
            // 遍历指定包及其子孙包中的所有文件，查找所有.class文件
            for (File file : dir.listFiles()) {
                if (file.isDirectory()) {
                    // 若当前遍历的file为目录，则递归调用当前方法
                    cacheClassName(packagePath + "." + file.getName());
                } else if (file.getName().endsWith(".class")) {
                    String simpleClassName = file.getName().replace(".class", "").trim();
                    // Servlet简单类名与Sevlet全限定类名对应的集合
                    servletNameMap.put(simpleClassName.toLowerCase(), packagePath + "." + simpleClassName);
                }
            }
            // 测试打印
            for (Map.Entry<String, String> entry : servletNameMap.entrySet()) {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            }
        } else {
            System.out.println("包名不能为空！");
            return;
        }
    }

    private void runServer() throws Exception {
        // boss线程组，用于处理连接
        EventLoopGroup bossgroup = new NioEventLoopGroup();
        // worker线程组，用于处理数据IO
        EventLoopGroup workergroup = new NioEventLoopGroup();
        try {
            // netty服务端启动类，配置netty参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 配置线程组，服务端需要2个线程组
            bootstrap.group(bossgroup, workergroup)
                    // 配置channel类型，使用异步的服务器端 TCP Socket 连接
                    .channel(NioServerSocketChannel.class)
                    // 配置存放请求的队列的长度
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 配置启用心跳机制来检测长连接的存活性，即客户端的存活性
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    // 配置netty的的channel，使用内部类ChannelInitializer创建
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        /**
                         * 初始化channel
                         * @param socketChannel
                         * @throws Exception
                         */
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 拿到channel的pipeline，用于添加自定义配置
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            // 添加HttpServerCodec可以同时添加encoder和decoder
                            pipeline.addLast(new HttpServerCodec())
                                    //添加编码器
//                                    .addLast("encoder", new ObjectEncoder())
                                    //添加解码器  构造方法第一个参数设置二进制数据的最大字节数  第二个参数设置具体使用哪个类解析器
//                                    .addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)))
                                    // 添加自定义的服务处理助手
                                    .addLast(new WebContainerHandler(servletMap, servletNameMap, staticPath));
                        }
                    });
            // 给netty绑定端口并等待建立socket链接
            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("网络容器已启动，监听端口号为：" + port);
            // 阻塞线程，防止程序运维完后关闭，直至socket连接关闭后才会关闭
            future.channel().closeFuture().sync();
        } finally {
            bossgroup.shutdownGracefully();
            workergroup.shutdownGracefully();
        }
    }

    /**
     * 从server.xml配置文件中读取端口号和Servlet所在包名
     *
     * @throws DocumentException
     */
    private void initContent() {
        //读取配置文件Server.xml
        InputStream in = WebServer.class.getClassLoader().getResourceAsStream("server.xml");
        //获取配置文件输入流
        SAXReader saxReader = new SAXReader();
        Document doc = null;
        try {
            doc = saxReader.read(in);
            //使用SAXReader + XPath读取端口配置
            Element portEle = (Element) doc.selectSingleNode("//port");
            Element packagePathEle = (Element) doc.selectSingleNode("//packagePath");
            Element staticPathEle = (Element) doc.selectSingleNode("//staticPath");
            this.port = Integer.valueOf(portEle.getText());
            this.packagePath = packagePathEle.getText();
            this.staticPath = staticPathEle.getText();
        } catch (DocumentException e) {
            System.out.println("读取server.xml文件错误！");
            e.printStackTrace();
        }

    }
}
